package com.gframework.boot.mvc.controller.protocal.basic;

import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.tomcat.util.http.fileupload.IOUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.MethodParameter;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotWritableException;
import org.springframework.web.HttpMediaTypeNotAcceptableException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
import org.springframework.web.method.support.ModelAndViewContainer;
import org.springframework.web.servlet.HandlerMapping;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter;
import org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor;

import com.gframework.boot.mvc.controller.errcode.ErrorCodeException;
import com.gframework.boot.mvc.controller.protocal.ControllerReturnValueInterceptor;
import com.gframework.boot.mvc.controller.protocal.RestResponseHandler;

/**
 * 前后端统一通讯格式controller代理类.
 * <p>本类的作用有三个：
 * <ul>
 * <li>1、简易处理文件下载</li>
 * <li>2、对返回值做增强处理扩展（返回值拦截器）</li>
 * <li>3、统一前后端通讯格式</li>
 * </ul>
 * <p>本类并不会进行任何异常的捕捉，所以对于异常的处理，你依然可以使用{@link ControllerAdvice}注解实现。
 * 并且如果你需要无论成功还是失败都有着一个统一的返回数据格式，那么本类就是非常必要的。如果你尝试通过{@link HandlerMethodReturnValueHandler}注解进行扩展，你会发现
 * 你无法控制spring mvc返回值处理器的顺序，其内置了十几个处理器，而你所添加的只是排在最后面，大多数时候根本没有走到你的处理器就已经结束了。
 * 
 * <p>{@link RequestResponseBodyMethodProcessor}也是一个spring mvc返回值处理器，他是专门处理{@link ResponseBody}类型的controller的。
 * 这就意味着如果你的类使用了{@link RestController}或者方法上带上了{@link ResponseBody}，那么就会由这个处理器处理。
 * 虽然这个处理器顺序排在很靠后的位置，但是依然优先于自定义返回值处理器。如果我们想要在一个使用了{@link RestController}注解的controller类中去返回一个能够被特定返回值处理器处理的返回值类型，那么将会无效。
 * <strong>而这一系列问题，也正是你要选择本类的原因。</strong>
 * 
 * <p>修改处理器的顺序是一个比较麻烦的过程，你需要对{@link RequestMappingHandlerAdapter}类型的spring bean进行修改，并需要声明一个新的List集合去覆盖原有的List集合。
 * <p><strong>本类的处理顺序默认是在{@link RequestResponseBodyMethodProcessor}处理器的前一个位置上</strong>，这就意味着如果你想继续使用spring mvc原有的那十几个最优先的返回值类型，依然可以使用不会冲突。
 * 而本类代理的类，也仅仅只是会被{@link RequestResponseBodyMethodProcessor}处理器所处理的方法。
 * 
 * <p>使用此AOP，你可以在rest风格的controller中进行文件下载和拦截器方式的返回值增强处理({@link ControllerReturnValueInterceptor}接口)。这使得controller编写会非常方便。
 * 
 * <p>真正定义统一返回格式的是{@link RestResponseHandler}接口，你可以通过创建一个此接口spring bean来覆盖默认的返回策略配置。默认将会采用{@link ServerResponseRestHandler}进行处理。
 * 
 * <p>前后端通讯格式统一不一定意味着在任何情况下返回内容完全一致，有时候成功的返回结果是不需要太多非业务信息的，这会对网络带宽有所影响，这个时候你可以通过配置"{@code gframework.mvc.noPackagingWhenSuccess=true}"
 * 来取消正常返回时不对返回结果进行非业务信息包装处理。或者你也可以通过自定义{@link RestResponseHandler}接口实现更为灵活的控制。
 * 
 * 
 * @since 1.0.0
 * @author Ghwolf
 * 
 * @see RequestResponseBodyMethodProcessor
 * @see HandlerMethodReturnValueHandler
 * @see RequestMappingHandlerAdapter
 * @see RestResponseHandler
 * @see ServerResponse ServerResponse 默认返回内容包装类
 * @see FileResponse FileResponse 文件下载返回类型
 * @see ErrorCodeException ErrorCodeException 用于表示异常状态的异常类
 * @see GFrameworkControllerAdvice GFrameworkControllerAdvice 默认的部分异常拦截器
 * @see GFrameworkErrorController GFrameworkErrorController 修改了spring默认的/error请求处理逻辑
 * 
 * @see HttpStatus
 */
@Order(10)
@Aspect
@SuppressWarnings("deprecation")
public class BasicProtocalControllerAOP extends RequestResponseBodyMethodProcessor {

	protected static final Logger slfLog = LoggerFactory.getLogger(BasicProtocalControllerAOP.class);
	/**
	 * 代理方法包含的注解1.<br>
	 * 类注解为RestController
	 */
	private static final String PROXY_METHOD_ANNOTATION1 = "@(org.springframework.web.bind.annotation.RequestMapping"
			+ " || org.springframework.web.bind.annotation.GetMapping"
			+ " || org.springframework.web.bind.annotation.PostMapping"
			+ " || org.springframework.web.bind.annotation.PutMapping"
			+ " || org.springframework.web.bind.annotation.PatchMapping"
			+ " || org.springframework.web.bind.annotation.DeleteMapping"
			+ ")";
	/**
	 * 代理方法包含的注解2.<br>
	 * 类注解为Controller，因此方法注解必须还要包含有ResponseBody
	 */
	private static final String PROXY_METHOD_ANNOTATION2 = "@org.springframework.web.bind.annotation.ResponseBody @(org.springframework.web.bind.annotation.RequestMapping"
			+ " || org.springframework.web.bind.annotation.GetMapping"
			+ " || org.springframework.web.bind.annotation.PostMapping"
			+ " || org.springframework.web.bind.annotation.PutMapping"
			+ " || org.springframework.web.bind.annotation.PatchMapping"
			+ " || org.springframework.web.bind.annotation.DeleteMapping"
			+ ")";
	/**
	 * 代理类包含的注解1:{@value}
	 */
	private static final String PROXY_CLASS_ANNOTATION1 = "@org.springframework.web.bind.annotation.RestController";
	/**
	 * 代理类包含的注解2:{@value}
	 */
	private static final String PROXY_CLASS_ANNOTATION2 = "@org.springframework.stereotype.Controller";
	
	/**
	 * 代理方法execution切入点表达式1.<br>
	 * 针对RestController注解定义的rest接口
	 */
	private static final String RESULT_CONTROLLER_AROUND_ASPECTJ1 = PROXY_METHOD_ANNOTATION1 + " * " + PROXY_CLASS_ANNOTATION1 + " *..*.*(..)";
	
	/**
	 * 代理方法execution切入点表达式2
	 * 针对Controller和ResponseBody注解定义的rest接口
	 */
	private static final String RESULT_CONTROLLER_AROUND_ASPECTJ2 = PROXY_METHOD_ANNOTATION2 + " * " + PROXY_CLASS_ANNOTATION2 + " *..*.*(..)";
	
	/**
	 * 最终组合出来的代理方法切入点表达式
	 */
	private static final String RESULT_CONTROLLER_ASPECTJ = "execution(" + RESULT_CONTROLLER_AROUND_ASPECTJ1 + ") || execution(" + RESULT_CONTROLLER_AROUND_ASPECTJ2 + ")";

	/**
	 * 默认下载文件名(已经经过URL编码)：未命名的文件
	 */
	private static final String DEFAULT_FILE_NAME = "%E6%9C%AA%E5%91%BD%E5%90%8D%E7%9A%84%E6%96%87%E4%BB%B6";
	
	/**
	 * 前后端统一通讯格式处理对象
	 */
	@Autowired
	private RestResponseHandler restResponseHandler ;
	
	/**
	 * 返回值拦截处理器
	 */
	@Resource
	private ControllerReturnValueInterceptor[] interceptors ;
	/**
	 * 当前HttpServletRequest类对象
	 */
	@Resource
	private HttpServletRequest request;
	
	/**
	 * 当前HttpServletResponse对象
	 */
	@Resource
	private HttpServletResponse response ;
	
	/**
	 * 返回值临时存储本地线程变量.
	 * <p>
	 * 当controller返回值是void的时候，AOP无论返回什么都是null，因此先临时将返回值存储起来，
	 * 等代码执行到ReturnValueHandler的时候，在将其设置上去即可。
	 * </p>
	 */
	private final ThreadLocal<Object> voidReturnValue = new ThreadLocal<>();
	
	/**
	 * 本类继承RequestResponseBodyMethodProcessor返回值类型处理类，因此实例化本类对象需要传入HttpMessageConverter集合.
	 * Spring在实例化本类对象时会自动注入此集合，因此这里可以不用管。
	 */
	public BasicProtocalControllerAOP(List<HttpMessageConverter<?>> converters) {
		super(converters);
		slfLog.info("====> 已启用 BasicProtocalControllerAOP 前后端统一通信格式拦截AOP");
	}
	
	/**
	 * 拦截controller的方法，但不会捕捉异常，异常依然需要走ControllerAdvice注解
	 */
	@Around(RESULT_CONTROLLER_ASPECTJ)
	public Object around(ProceedingJoinPoint join) throws Throwable{
		
		Object result = join.proceed(join.getArgs());
		result = resultInterceptor(result);
		
		if (result instanceof FileResponse) {
			// 文件下载
			handleResultToFile((FileResponse)result);
			return null ;
		}
		
		result = restResponseHandler.handleResult(result, request, response);
		
		// 判断是否设置了produces
		Object produces = request.getAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);
		if (produces == null || (produces instanceof Set && ((Set<?>)produces).isEmpty()) ) {
			Set<MediaType> set = new HashSet<>();
			// 默认采用json utf-8的格式
			set.add(MediaType.APPLICATION_JSON_UTF8);
			request.setAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE,set);
		}
		
		Signature sign = join.getSignature();
		boolean isVoidReturnValue = this.saveIfVoid(sign, result);
		return isVoidReturnValue ? null : result ;
	}
	
	/**
	 * 返回结果优先拦截处理操作.
	 * <p>
	 * <strong>这是取得返回结果后（不抛异常情况下）的第一层拦截器</strong>
	 * 此方法不会抛出异常，但可能改变结果值
	 * </p>
	 * @param result 返回结果
	 * @return 如果有处理，则返回处理后的对象，否则返回原始对象
	 */
	private Object resultInterceptor(Object result) {
		if (result == null) return result ;
		for (ControllerReturnValueInterceptor inter : this.interceptors) {
			if (inter.supportInterceptor(result.getClass())) {
				result = inter.doInterceptor(result);
				if (result == null) return null ;
			}
		}
		return result ;
	}
	
	/**
	 * 处理文件下载逻辑
	 * @param file {@link FileBody}类对象
	 */
	private void handleResultToFile(FileResponse file) {
		String type = file.getMimeType();
		if (type == null) {
			if (slfLog.isWarnEnabled()) {
				slfLog.warn("未设置文件类型：{}",file.getFileName());
			}
			type = file.isShowInBrowser() ? MediaType.APPLICATION_OCTET_STREAM_VALUE : MediaType.MULTIPART_FORM_DATA_VALUE ;
		}
		String encodingFileName = null;
		if (file.getFileName() == null) {
			if (slfLog.isWarnEnabled()) {
				slfLog.warn("文件未指定名称。即便是在浏览器显示文件，如图片或pdf，也应该设置名称，如果浏览器不支持显示，那么会变成下载文件。");
			}
			encodingFileName = DEFAULT_FILE_NAME;
		} else {
			try {
				encodingFileName = URLEncoder.encode(file.getFileName(),"UTF-8");
			} catch (UnsupportedEncodingException e) {}
		}
		InputStream input = file.getDataInputStream();

		this.response.setContentType(type);
		this.response.setHeader("Content-Disposition", (file.isShowInBrowser() ? "inline" : "attachment") + ";fileName=" + encodingFileName);
		if (file.getDataLength() != null) {
			this.response.setContentLengthLong(file.getDataLength());
		}
		
		if (input != null) {
			try {
				IOUtils.copy(input,this.response.getOutputStream());
			} catch (IOException e) {
				if (slfLog.isErrorEnabled()) {
					slfLog.error("{}文件失败：" + file.getFileName(),file.isShowInBrowser() ? "显示" : "下载",e);
				}
			}
		}
	}
	
	/**
	 * 本方法负责判断当前代理方法的返回值是否是void，并针对void返回值的数据做出相应的处理（也能返回内容）.
	 * <p>
	 * 如果是void，则返回true，并将结果缓存。否则什么都不做并返回false
	 * </p>
	 * @param sign 代理切入点对象签名信息
	 * @param body 原本要返回的数据
	 * @return 如果返回值是void，则返回true，否则返回false
	 */
	private boolean saveIfVoid(Signature sign,Object body) {
		if (sign instanceof MethodSignature) {
			MethodSignature ms = (MethodSignature) sign ;
			if (ms.getReturnType() == void.class) {
				this.voidReturnValue.set(body);
				return true ;
			}
		}
		return false ;
	}
	
	
	
	/**
	 * 在处理返回值的时候，会首先判断返回值是否是void，如果是，则将AOP中生成的数据对象返回。
	 */
	@Override
	public void handleReturnValue(Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer,
			NativeWebRequest webRequest)
					throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {
		Object obj = returnValue ;
		if (returnType.getMethod().getReturnType() == void.class) {
			obj = this.voidReturnValue.get();
			this.voidReturnValue.remove();
		}
		super.handleReturnValue(obj, returnType, mavContainer, webRequest);
	}

	
	/**
	 * spring针对返回统一处理的操作类
	 */
    @Resource
    private RequestMappingHandlerAdapter requestMappingHandlerAdapter;
	
	/**
	 * 自定义BasicProtocalControllerAOP同时修改顺序.
	 * <p>
	 * 即便设置了returnValueHandler，也很难执行到这里，因为系统默认有15个返回值处理器，自定义的排在非常靠后的位置。
	 * 因此需要通过重新设置排序，将其设置到ResponseBody的前面。
	 * </p>
	 */
	@PostConstruct
	private void changeReturnValueHandleOrder() {
		List<HandlerMethodReturnValueHandler> handle = this.requestMappingHandlerAdapter.getReturnValueHandlers();
		List<HandlerMethodReturnValueHandler> list = new ArrayList<>(handle.size() + 1);
		for (HandlerMethodReturnValueHandler vo : handle) {
			if (vo == null) continue ;
			if (vo.getClass() == RequestResponseBodyMethodProcessor.class) {
				list.add(this);
			}
			list.add(vo);
		}
		this.requestMappingHandlerAdapter.setReturnValueHandlers(Collections.unmodifiableList(list));
	}

	
}
